package fr.limsi.seven;

import java.awt.AWTException;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Robot;
import java.awt.TexturePaint;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;
import java.util.Map.Entry;

import javax.imageio.ImageIO;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

/**
 * @author Camille
 */
public class Pyramide2D extends JPanel implements MouseListener, MouseWheelListener, MouseMotionListener, KeyListener {

	/**
	 * Auto-generated serial version UID number
	 */
	public static final long serialVersionUID = -4628602644687182253L;

	/**
	 * The {@link PyramideViewer2D} the view is assigned to
	 */
	protected PyramideViewer2D viewer;

	/**
	 * Associated {@link Pyramide}
	 */
	protected Pyramide pyramide;

	/**
	 * <i>Pyramide2D</i> base
	 */
	protected LinkedList<Node2D> base;

	/**
	 * <i>Pyramide2D</i> nodes ordered by level
	 */
	protected LinkedList<LinkedList<Node2D>> levels;

	/**
	 * Hashmap of the <i>Pyramide2D</i> nodes (associates id and node)
	 */
	protected HashMap<Integer, Node2D> nodes;

	/**
	 * Hashmap associating a {@link PyramideNode} with its node in the view
	 */
	protected HashMap<PyramideNode, Node2D> assocNodes;

	/**
	 * Y delta for the pyramide summit point coordinate
	 */
	public static final int SUMMIT_DELTA = 5;

	/**
	 * Triangular textures epsilon (error correction for trigonometric approximation)
	 */
	public static final float TRIANGULAR_EPSILON = 1.5f;

	/**
	 * Viewer Margins
	 */
	public static final int MARGINS = 10;

	/**
	 * User labels margins
	 */
	public static final int LABEL_MARGINS = 5;

	/**
	 * User labels borders
	 */
	public static final int LABEL_BORDERS = 10;

	/**
	 * Translation step when user wants to move the view manually
	 */
	public static final int USER_TRANSLATION_STEP = 10;

	/**
	 * Scaling factor when user wants to zoom in or out the view manually
	 */
	public static final float USER_SCALE_STEP = 1.1f;

	/**
	 * Alpha composite for fading effect
	 */
	public static final float FADING = 0.2f;

	/**
	 * Texture size
	 */
	public static final int TEXTURE_SIZE = 100;

	/**
	 * Fog
	 */
	public static final Color FOG = new Color(0.7f, 0.7f, 0.7f, 0.8f);

	/**
	 * All {@link Link2D} objects showed
	 */
	protected LinkedList<Link2D> links;

	/**
	 * View {@link Annotation} list
	 */
	protected Vector<Annotation> annotations;

	/**
	 * {@link Barrel} for the view
	 */
	protected Barrel barrel;

	/**
	 * {@link Shelf2D} for the view
	 */
	protected Shelf2D shelf;

	/**
	 * {@link Grid2D} for the view
	 */
	protected Grid2D grid;

	/**
	 * Brick texture
	 */
	protected BufferedImage brickTexture;

	/**
	 * Marble texture
	 */
	protected BufferedImage marbleTexture;

	/**
	 * Pyramide background texture {@link Area}
	 */
	protected Area backgroundTexturePath;

	/**
	 * Node family distance texture paths
	 */
	protected LinkedList<GeneralPath> distanceTexturePaths;

	/**
	 * Brick texture paint
	 */
	protected TexturePaint brickPaint;

	/**
	 * Marble texture paint
	 */
	protected TexturePaint marblePaint;

	/**
	 * Animations builder for the view
	 */
	protected Pyramide2DAnimator animationBuilder;

	/**
	 * Animation monitor for the view
	 */
	protected AnimationMonitor animationMonitor;

	/**
	 * Viewer search tool
	 */
	protected Pyramide2DSearchTool searchTool;

	/**
	 * Animation call thread for the view
	 */
	protected Thread animationLauncher;

	/**
	 * Animation indicator for the <i>animationLauncher</i>
	 */
	protected boolean animate = true;

	/**
	 * Translating state indicator
	 */
	protected boolean translating;

	/**
	 * Current view translation
	 */
	protected Point translation;

	/**
	 * Scaling state indicator
	 */
	protected boolean scaling;

	/**
	 * Current view scale
	 */
	protected float scale;

	/**
	 * Dragging indicator
	 */
	protected boolean dragging;

	/**
	 * Mouse x coordinate
	 */
	int mouseX;

	/**
	 * Mouse y coordinate
	 */
	int mouseY;

	/**
	 * Timer for the actions
	 */
	protected Timer actionTimer;

	/**
	 * Miniview for the current view
	 */
	protected MiniView2D miniView;

	/**
	 * Constructs a {@link Pyramide} 2D viewer
	 * 
	 * @param pyramide
	 *            the {@link Pyramide} to view
	 */
	public Pyramide2D(PyramideViewer2D viewer, Pyramide pyramide) {
		super();

		this.pyramide = pyramide;

		this.viewer = viewer;
		this.base = new LinkedList<Node2D>();
		this.levels = new LinkedList<LinkedList<Node2D>>();

		setDoubleBuffered(true);
		setOpaque(true);

		setPreferredSize(new Dimension(3 * (PyramideViewer2D.DEFAULT_WIDTH / 4), PyramideViewer2D.DEFAULT_HEIGHT));

		addMouseListener(this);
		addMouseWheelListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);

		miniView = null;

		annotations = new Vector<Annotation>();

		barrel = new Barrel(this);
		shelf = new Shelf2D(this);

		backgroundTexturePath = new Area();
		distanceTexturePaths = new LinkedList<GeneralPath>();

		try {
			brickTexture = ImageIO.read(getClass().getResource("/fr/limsi/seven/images/brick.jpg"));
			marbleTexture = ImageIO.read(getClass().getResource("/fr/limsi/seven/images/marble.jpg"));

			brickPaint = new TexturePaint(brickTexture, new Rectangle2D.Float(0, 0, TEXTURE_SIZE, TEXTURE_SIZE));
			marblePaint = new TexturePaint(marbleTexture, new Rectangle2D.Float(0, 0, TEXTURE_SIZE, TEXTURE_SIZE));
		}
		catch (IOException e) {
			e.printStackTrace();
		}

		translating = false;
		translation = new Point(0, 0);
		scale = 1.0f;

		nodes = new HashMap<Integer, Node2D>();
		links = new LinkedList<Link2D>();
		assocNodes = new HashMap<PyramideNode, Node2D>();

		buildView();

		grid = new Grid2D(this);

		searchTool = new Pyramide2DSearchTool(this);

		animationMonitor = AnimationMonitor.getInstance();
		animationBuilder = new Pyramide2DAnimator(this);
		animationLauncher = new Thread() {
			@Override
			public void run() {
				while (animate) {
					animationMonitor.playAnimation();
				}
			}
		};

		animationLauncher.start();
	}

	/**
	 * Builds the view (creates {@link Node2D} and {@link Link2D} elements)
	 */
	public void buildView() {

		/* Constructing nodes for base level */
		for (PyramideNode node : pyramide.getBase()) {
			base.add(buildNode2D(node));
		}

		/* Constructing nodes for other levels */
		for (LinkedList<PyramideNode> level : pyramide.getLevelsList()) {

			LinkedList<Node2D> level2D = new LinkedList<Node2D>();

			for (PyramideNode node : level) {
				level2D.add(buildNode2D(node));
			}

			levels.add(level2D);
		}

		/* Constructing links for all nodes */
		for (LinkedList<PyramideNode> level : pyramide.getLevelsList()) {
			for (PyramideNode node : level) {
				buildLinks(node);
			}
		}
	}

	/**
	 * Creates a {@link Node2D} for the view, associated it to a {@link PyramideNode}
	 * 
	 * @param node
	 * @param x
	 * @param y
	 */
	protected Node2D buildNode2D(PyramideNode node) {

		Node2D node2D = null;

		try {

			ArrayList<PyramideAbstractVariableNode> nodeVars = new ArrayList<PyramideAbstractVariableNode>();

			for (int i = 0; i < node.getVariables().size(); i++) {
				nodeVars.add(node.getVariables().elementAt(i));
			}

			node2D = new Node2D(this, node);

			add(node2D);
			nodes.put(node.getID(), node2D);
			assocNodes.put(node, node2D);

		}
		catch (PyramideNodeException e) {
			e.printStackTrace();
		}

		return node2D;
	}

	/**
	 * Create {@link Link2D} between the Node2D associated to a {@link PyramideNode} and its children
	 * 
	 * @param parent
	 */
	protected void buildLinks(PyramideNode parent) {

		Node2D parent2D = assocNodes.get(parent);
		for (PyramideNode child : parent.getChildren()) {
			Node2D child2D = assocNodes.get(child);
			addLinkBetween(parent2D, child2D);
		}
	}

	/**
	 * Creates a {@link Link2D} between two {@link Node2D}
	 * 
	 * @param parent
	 * @param child
	 */
	protected void addLinkBetween(Node2D parent, Node2D child) {
		Link2D link = new Link2D(this, parent, child);
		parent.addChildLink(link);
		child.addParentLink(link);
		links.add(link);
		add(link);
	}

	/**
	 * (Re)positions all the {@link Node2D} (called on windows resize, etc.)
	 */
	public void positionNodes() {

		float x;
		float y;
		float nodeOccupiedSpace = 0;
		float baseNodeMaxSize = Float.NEGATIVE_INFINITY;

		/* 1 - Base level repositioning */
		for (Node2D node2D : base) {
			nodeOccupiedSpace += node2D.getNodeSize();
			baseNodeMaxSize = Math.max(baseNodeMaxSize, node2D.getNodeSize());
		}

		float spacing = (getWidth() - (2 * MARGINS) - nodeOccupiedSpace) / (base.size() - 1);
		x = MARGINS; // first base node x coordinate

		for (Node2D node2D : base) {

			y = getHeight() - node2D.getNodeSize() - MARGINS;
			node2D.setAbsolutePosition((int) x, (int) y);

			x += node2D.getNodeSize() + spacing; // Increasing x coordinate for next node
		}

		/* 2.2 - Other levels repositioning */
		double pyramideHeight = pyramide.getHeight();

		for (LinkedList<Node2D> level : levels) {
			for (Node2D node2D : level) {

				try {
					x = calculateX(node2D.getPNode());

					if (grid.getScaleType() == Grid2D.LOGARITHMIC_SCALE) {
						y = (getHeight() - MARGINS - node2D.getNodeSize()) - ((float) (Math.log1p(node2D.getPNode().getHeight() * grid.getRangeFactor()) / Math.log1p(pyramideHeight * grid.getRangeFactor())) * (getHeight() - (2 * MARGINS)));
					}
					else {
						y = (getHeight() - MARGINS - node2D.getNodeSize()) - ((float) (node2D.getPNode().getHeight() / pyramideHeight) * (getHeight() - (2 * MARGINS)));
					}

					node2D.setAbsolutePosition((int) x, (int) y);
				}
				catch (PyramideViewer2DException e) {
					e.printStackTrace();
				}
			}
		}

		positionSecondaryObjects();
		repaint();
	}

	/**
	 * Reset all showing {@link StarDrawer} bounds (useful when resizing)
	 */
	public void reboundsStars() {
		for (Iterator<Entry<Integer, Node2D>> entries = getNodes().entrySet().iterator(); entries.hasNext();) {

			Entry<Integer, Node2D> entry = entries.next();
			Node2D node2D = entry.getValue();

			StarDrawer sD = node2D.getStarDrawer();

			if (sD.isShowing()) {
				Point pos = new Point(node2D.getAbsolutePosition().x, node2D.getAbsolutePosition().y);
				applyTransformations(pos);

				sD.setBounds(pos.x - (sD.getMaxWidth() / 2), pos.y - (sD.getMaxHeight() / 2), sD.getMaxWidth(), sD.getMaxHeight());
			}
		}
	}

	/**
	 * Positions secondary objects (links, textures path, annotations, ...)
	 */
	public void positionSecondaryObjects() {

		for (Link2D link : links) {
			link.resetBounds();

			if (link.getUserLabel().isShowing()) {

				Rectangle2D labelBBox = link.getUserLabel().getFontMetrics(link.getUserLabel().getFont()).getStringBounds(link.getUserLabel().getText(), getGraphics());

				int labelWidth = (int) labelBBox.getWidth();
				int labelHeight = (int) labelBBox.getHeight();

				link.getUserLabel().setBounds(link.getX() + (link.getWidth() / 2), link.getY() + ((link.getHeight() - labelHeight) / 2), labelWidth + LABEL_BORDERS, labelHeight + LABEL_BORDERS);
			}

			link.repaint();
		}

		for (Annotation annotation : annotations) {

			Point position = new Point(annotation.getAbsolutePosition().x, annotation.getAbsolutePosition().y);
			applyTransformations(position);

			annotation.setBounds(position.x, position.y, (int) (annotation.getAbsoluteWidth() * scale), (int) (annotation.getAbsoluteHeight() * scale));

			updateAnnotation(annotation);
		}

		if (texturesEnabled()) {
			traceAllTexturePaths();
		}
	}

	@Override
	public void paint(Graphics g) {
		paintComponent(g);
	}

	@Override
	protected void paintComponent(Graphics g) {

		Graphics2D g2 = (Graphics2D) g;

		g2.clearRect(0, 0, getWidth(), getHeight());

		g2.setPaint(Color.WHITE);
		g2.fillRect(0, 0, getWidth(), getHeight());
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

		/* Drawing */
		draw(g2);
		paintChildren(g2);

		/* Draw search tool focus area (if a search is made) */
		if (searchTool.isSearching()) {
			searchTool.drawFocusArea(g2);
		}
	}

	/**
	 * Draw the view
	 * 
	 * @param g2
	 *            the Graphics2D element used for the painting
	 */
	protected void draw(Graphics2D g2) {

		g2.setColor(getDrawingColor());

		if (gridEnabled()) {
			grid.drawGrid(g2);
		}

		/* Rendering nodes */
		/* Rendering base nodes */
		for (Node2D node2D : base) {
			Point position = new Point(node2D.getAbsolutePosition().x, node2D.getAbsolutePosition().y);
			applyTransformations(position);
			node2D.setBounds(position.x, position.y, (int) (node2D.getNodeSize() * scale), (int) (node2D.getNodeSize() * scale));

			/* User label */
			if (node2D.getUserLabel().isShowing()) {

				Rectangle2D labelBBox = node2D.getUserLabel().getFontMetrics(node2D.getUserLabel().getFont()).getStringBounds(node2D.getUserLabel().getText(), this.getGraphics());

				int labelWidth = (int) labelBBox.getWidth();
				int labelHeight = (int) labelBBox.getHeight();

				node2D.getUserLabel().setBounds(position.x + ((int) (node2D.getNodeSize() * scale) / 2) - (labelWidth / 2), position.y + (int) (node2D.getNodeSize() * scale) + LABEL_MARGINS, labelWidth + LABEL_BORDERS, labelHeight + LABEL_BORDERS);
			}

			/* Star Drawer */
			if (node2D.getStarDrawer().isShowing()) {
				g2.setPaint(node2D.getColor());
				g2.setStroke(Pyramide.DASHED_STROKE);
				g2.drawLine((int) (node2D.getX() + ((node2D.getNodeSize() * scale) / 2)), (int) (node2D.getY() + ((node2D.getNodeSize() * scale) / 2)), node2D.getStarDrawer().getX() + (node2D.getStarDrawer().getWidth() / 2), node2D.getStarDrawer().getY() + (node2D.getStarDrawer().getHeight() / 2));
			}

			/* Unread note indicator */
			if (node2D.hasUnreadNote()) {
				node2D.showUnreadedNoteIcon();
			}
		}

		/* Rendering other nodes */
		for (LinkedList<Node2D> level : levels) {
			for (Node2D node2D : level) {
				Point position = new Point(node2D.getAbsolutePosition().x, node2D.getAbsolutePosition().y);
				applyTransformations(position);
				node2D.setBounds(position.x, position.y, (int) (node2D.getNodeSize() * scale), (int) (node2D.getNodeSize() * scale));

				/* User label */
				if (node2D.getUserLabel().isShowing()) {

					Rectangle2D labelBBox = node2D.getUserLabel().getFontMetrics(node2D.getUserLabel().getFont()).getStringBounds(node2D.getUserLabel().getText(), this.getGraphics());

					int labelWidth = (int) labelBBox.getWidth();
					int labelHeight = (int) labelBBox.getHeight();

					node2D.getUserLabel().setBounds(position.x + ((int) (node2D.getNodeSize() * scale) / 2) - (labelWidth / 2), position.y + (int) (node2D.getNodeSize() * scale) + LABEL_MARGINS, labelWidth + LABEL_BORDERS, labelHeight + LABEL_BORDERS);
				}

				/* Star Drawer */
				if (node2D.getStarDrawer().isShowing()) {
					g2.setPaint(node2D.getColor());
					g2.setStroke(Pyramide.DASHED_STROKE);
					g2.drawLine((int) (node2D.getX() + ((node2D.getNodeSize() * scale) / 2)), (int) (node2D.getY() + ((node2D.getNodeSize() * scale) / 2)), node2D.getStarDrawer().getX() + (node2D.getStarDrawer().getWidth() / 2), node2D.getStarDrawer().getY() + (node2D.getStarDrawer().getHeight() / 2));
				}

				/* Unread note indicator */
				if (node2D.hasUnreadNote()) {
					node2D.showUnreadedNoteIcon();
				}
			}
		}

		/* Handling textures */
		if (texturesEnabled()) {

			/* Outer textures */
			if (brickTexture != null) {

				Composite oldComposite = g2.getComposite();
				Composite newComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (((float) getDrawingColor().getAlpha()) / 255.0f));

				g2.setComposite(newComposite);
				g2.setPaint(brickPaint);

				g2.fill(backgroundTexturePath);

				g2.setComposite(oldComposite);
			}

			/* Inner textures */
			if (marbleTexture != null) {
				Composite oldComposite = g2.getComposite();
				Composite newComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (((float) getDrawingColor().getAlpha()) / 255.0f) * 0.9f);
				g2.setComposite(newComposite);
				g2.setPaint(marblePaint);

				for (GeneralPath distanceTexturePath : distanceTexturePaths) {
					g2.fill(distanceTexturePath);
				}

				g2.setComposite(oldComposite);
			}
		}

		/* Mini view */
		if (miniView != null) {
			miniView.repaint();
		}
	}

	/**
	 * (Re)trace all texture paths
	 */
	public void traceAllTexturePaths() {

		backgroundTexturePath = createTextureArea();

		if (distanceTexturePaths != null) {
			distanceTexturePaths.clear();
		}

		for (Iterator<Entry<PyramideNode, Node2D>> iter = assocNodes.entrySet().iterator(); iter.hasNext();) {

			Entry<PyramideNode, Node2D> entry = iter.next();
			PyramideNode node = entry.getKey();
			Node2D node2D = entry.getValue();

			if (node.getChildren() != null && !node.getChildren().isEmpty()) {
				distanceTexturePaths.add(traceTexturePathFor(node2D));
			}
		}
	}

	/**
	 * Traces the texture {@link Area} for the {@link Pyramide}
	 * 
	 * @return the {@link Area} corresponding to the texture bounding shape
	 */
	protected Area createTextureArea() {
		switch (getLinkShape()) {

		case Link2D.TRIANGULAR:
			return createTriangularTextureArea();

		case Link2D.TRAPEZOIDAL:
			return createTrapezoidalTextureArea();

		default:
			return createTriangularTextureArea();
		}
	}

	/**
	 * Traces the texture shape {@link GeneralPath} for a {@link Node2D}
	 * 
	 * @param node
	 * @return the {@link GeneralPath} corresponding to the texture bounding shape
	 */
	protected GeneralPath traceTexturePathFor(Node2D node) {
		switch (getLinkShape()) {

		case Link2D.TRIANGULAR:
			return traceTriangularTexturePathFor(node);

		case Link2D.TRAPEZOIDAL:
			return traceTrapezoidalTexturePathFor(node);

		default:
			return traceTriangularTexturePathFor(node);
		}
	}

	/**
	 * Traces the triangular texture shape {@link Area} for the {@link Pyramide}
	 * 
	 * @return a {@link Area} object corresponding to the {@link Pyramide} bounding shape
	 */
	protected Area createTriangularTextureArea() {

		Node2D summit = levels.getLast().getLast();
		Node2D firstBaseNode = base.getFirst();
		Node2D lastBaseNode = base.getLast();

		Point start = new Point(summit.getAbsolutePosition().x, summit.getAbsolutePosition().y);
		applyTransformations(start);

		Point firstBase = new Point(firstBaseNode.getAbsolutePosition().x, firstBaseNode.getAbsolutePosition().y);
		applyTransformations(firstBase);

		Point lastBase = new Point(lastBaseNode.getAbsolutePosition().x, lastBaseNode.getAbsolutePosition().y);
		applyTransformations(lastBase);

		/* 1 - Tracing left side of the texture */
		GeneralPath texturePathLeft = new GeneralPath();

		texturePathLeft.moveTo(start.x + ((summit.getNodeSize() * scale) / 2), start.y - SUMMIT_DELTA);
		texturePathLeft.lineTo(start.x, start.y);

		Node2D current = summit;

		while (current.getLinksOut() != null && !current.getLinksOut().isEmpty()) {

			Node2D nodeOnLeft = current.getLinksOut().getFirst().getChildNode();

			for (Link2D link : current.getLinksOut()) {
				Node2D contender = link.getChildNode();

				if (nodeOnLeft.getAbsolutePosition().x > contender.getAbsolutePosition().x) {
					nodeOnLeft = contender;
				}
			}

			Point leftPoint = new Point(nodeOnLeft.getAbsolutePosition().x, nodeOnLeft.getAbsolutePosition().y);
			applyTransformations(leftPoint);

			texturePathLeft.lineTo(leftPoint.x, leftPoint.y);

			current = nodeOnLeft;
		}

		texturePathLeft.lineTo(firstBase.x - SUMMIT_DELTA, firstBase.y + (firstBaseNode.getNodeSize() * scale));
		texturePathLeft.lineTo(start.x + ((lastBaseNode.getNodeSize() * scale) / 2), firstBase.y + (firstBaseNode.getNodeSize() * scale));

		texturePathLeft.closePath();

		/* 2 - Tracing right side of the texture */
		GeneralPath texturePathRight = new GeneralPath();

		texturePathRight.moveTo(start.x + ((summit.getNodeSize() * scale) / 2), start.y - SUMMIT_DELTA);
		texturePathRight.lineTo(start.x + (summit.getNodeSize() * scale), start.y);

		current = summit;

		while (current.getLinksOut() != null && !current.getLinksOut().isEmpty()) {

			Node2D nodeOnRight = current.getLinksOut().getFirst().getChildNode();

			for (Link2D link : current.getLinksOut()) {
				Node2D contender = link.getChildNode();

				if (nodeOnRight.getAbsolutePosition().x < contender.getAbsolutePosition().x) {
					nodeOnRight = contender;
				}
			}

			Point rightPoint = new Point(nodeOnRight.getAbsolutePosition().x, nodeOnRight.getAbsolutePosition().y);
			applyTransformations(rightPoint);

			texturePathRight.lineTo(rightPoint.x + (nodeOnRight.getNodeSize() * scale), rightPoint.y);

			current = nodeOnRight;
		}

		texturePathRight.lineTo(lastBase.x + (lastBaseNode.getNodeSize() * scale) + SUMMIT_DELTA, lastBase.y + (lastBaseNode.getNodeSize() * scale));
		texturePathRight.lineTo(start.x + ((lastBaseNode.getNodeSize() * scale) / 2), lastBase.y + (lastBaseNode.getNodeSize() * scale));

		texturePathRight.closePath();

		/* 3 - Adding the two pathes to create the texture area */
		Area textureArea = new Area(texturePathLeft);
		textureArea.add(new Area(texturePathRight));

		return textureArea;
	}

	/**
	 * Traces the triangular texture shape {@link GeneralPath} for a {@link Node2D}
	 * 
	 * @return a {@link GeneralPath} object corresponding to the {@link Node2D} family bounding shape
	 */
	protected GeneralPath traceTriangularTexturePathFor(Node2D node) {
		GeneralPath texturePath = new GeneralPath();

		float xLeft = Float.POSITIVE_INFINITY;
		float xRight = Float.NEGATIVE_INFINITY;
		float yLeft = 0;
		float yRight = 0;
		float yMin = Float.POSITIVE_INFINITY;

		for (PyramideNode child : node.getPNode().getChildren()) {

			Node2D child2D = assocNodes.get(child);
			Point cPos = new Point(child2D.getAbsolutePosition().x, child2D.getAbsolutePosition().y);
			applyTransformations(cPos);

			if (cPos.x < xLeft) {
				xLeft = cPos.x + (float) ((child2D.getNodeSize() * scale) * ((1 / 2) + (Math.sqrt(2) / 2)));
				yLeft = cPos.y + (float) ((child2D.getNodeSize() * scale) * ((1 / 2) + (Math.sqrt(2) / 2)));
				yMin = Math.min(yMin, yLeft);
			}

			if (cPos.x > xRight) {
				xRight = cPos.x;
				yRight = cPos.y + (float) ((child2D.getNodeSize() * scale) * ((1 / 2) + (Math.sqrt(2) / 2)));
				yMin = Math.min(yMin, yRight);
			}
		}

		Point nPos = new Point(node.getAbsolutePosition().x, node.getAbsolutePosition().y);
		applyTransformations(nPos);

		texturePath.moveTo(nPos.x, nPos.y + (node.getNodeSize() * scale));

		texturePath.lineTo(xLeft + (TRIANGULAR_EPSILON * scale), yMin);
		texturePath.lineTo(xRight, yMin);

		texturePath.lineTo(nPos.x + (node.getNodeSize() * scale), nPos.y + (node.getNodeSize() * scale));

		return texturePath;
	}

	/**
	 * Traces the trapezoidal texture shape {@link Area} for the {@link Pyramide}
	 * 
	 * @return a {@link Area} object corresponding to the {@link Pyramide} bounding shape
	 */
	protected Area createTrapezoidalTextureArea() {
		Node2D summit = levels.getLast().getLast();
		Node2D firstBaseNode = base.getFirst();
		Node2D lastBaseNode = base.getLast();

		Point start = new Point(summit.getAbsolutePosition().x, summit.getAbsolutePosition().y);
		applyTransformations(start);

		Point firstBase = new Point(firstBaseNode.getAbsolutePosition().x, firstBaseNode.getAbsolutePosition().y);
		applyTransformations(firstBase);

		Point lastBase = new Point(lastBaseNode.getAbsolutePosition().x, lastBaseNode.getAbsolutePosition().y);
		applyTransformations(lastBase);

		/* 1 - Tracing left side of the texture */
		GeneralPath texturePathLeft = new GeneralPath();

		texturePathLeft.moveTo(start.x + ((summit.getNodeSize() * scale) / 2), start.y);

		Node2D current = summit;
		Point currentPoint = new Point(current.getAbsolutePosition().x, current.getAbsolutePosition().y);
		applyTransformations(currentPoint);

		while (current.getLinksOut() != null && !current.getLinksOut().isEmpty()) {

			Node2D nodeOnLeft = current.getLinksOut().getFirst().getChildNode();

			for (Link2D link : current.getLinksOut()) {
				Node2D contender = link.getChildNode();

				if (nodeOnLeft.getAbsolutePosition().x > contender.getAbsolutePosition().x) {
					nodeOnLeft = contender;
				}
			}

			Point leftPoint = new Point(nodeOnLeft.getAbsolutePosition().x, nodeOnLeft.getAbsolutePosition().y);
			applyTransformations(leftPoint);

			texturePathLeft.lineTo(leftPoint.x + (Link2D.TRAPEZOID_DELTA * scale), currentPoint.y);
			texturePathLeft.lineTo(leftPoint.x, leftPoint.y);

			current = nodeOnLeft;
			currentPoint = new Point(current.getAbsolutePosition().x, current.getAbsolutePosition().y);
			applyTransformations(currentPoint);
		}

		texturePathLeft.lineTo(firstBase.x - (Link2D.TRAPEZOID_DELTA * scale), firstBase.y + (firstBaseNode.getNodeSize() * scale));
		texturePathLeft.lineTo(start.x + ((lastBaseNode.getNodeSize() * scale) / 2), firstBase.y + (firstBaseNode.getNodeSize() * scale));

		texturePathLeft.closePath();

		/* 2 - Tracing right side of the texture */
		GeneralPath texturePathRight = new GeneralPath();

		texturePathRight.moveTo(start.x + ((summit.getNodeSize() * scale) / 2), start.y);

		current = summit;
		currentPoint = new Point(current.getAbsolutePosition().x, current.getAbsolutePosition().y);
		applyTransformations(currentPoint);

		while (current.getLinksOut() != null && !current.getLinksOut().isEmpty()) {

			Node2D nodeOnRight = current.getLinksOut().getFirst().getChildNode();

			for (Link2D link : current.getLinksOut()) {
				Node2D contender = link.getChildNode();

				if (nodeOnRight.getAbsolutePosition().x < contender.getAbsolutePosition().x) {
					nodeOnRight = contender;
				}
			}

			Point rightPoint = new Point(nodeOnRight.getAbsolutePosition().x, nodeOnRight.getAbsolutePosition().y);
			applyTransformations(rightPoint);

			texturePathRight.lineTo(rightPoint.x + (nodeOnRight.getNodeSize() * scale) - (Link2D.TRAPEZOID_DELTA * scale), currentPoint.y);
			texturePathRight.lineTo(rightPoint.x + (nodeOnRight.getNodeSize() * scale), rightPoint.y);

			current = nodeOnRight;
			currentPoint = new Point(current.getAbsolutePosition().x, current.getAbsolutePosition().y);
			applyTransformations(currentPoint);
		}

		texturePathRight.lineTo(lastBase.x + (lastBaseNode.getNodeSize() * scale) + (Link2D.TRAPEZOID_DELTA * scale), lastBase.y + (lastBaseNode.getNodeSize() * scale));
		texturePathRight.lineTo(start.x + ((lastBaseNode.getNodeSize() * scale) / 2), lastBase.y + (lastBaseNode.getNodeSize() * scale));

		texturePathRight.closePath();

		/* 3 - Adding the two pathes to create the texture area */
		Area textureArea = new Area(texturePathLeft);
		textureArea.add(new Area(texturePathRight));

		return textureArea;
	}

	/**
	 * Traces the trapezoidal texture shape {@link GeneralPath} for a {@link Node2D}
	 * 
	 * @return a {@link GeneralPath} object corresponding to the {@link Node2D} family bounding shape
	 */
	protected GeneralPath traceTrapezoidalTexturePathFor(Node2D node) {
		GeneralPath texturePath = new GeneralPath();

		float xLeft = Float.POSITIVE_INFINITY;
		float xRight = Float.NEGATIVE_INFINITY;
		float yLeft = 0;
		float yRight = 0;
		float yMin = Float.POSITIVE_INFINITY;

		for (PyramideNode child : node.getPNode().getChildren()) {

			Node2D child2D = assocNodes.get(child);
			Point cPos = new Point(child2D.getAbsolutePosition().x, child2D.getAbsolutePosition().y);
			applyTransformations(cPos);

			if (cPos.x < xLeft) {
				xLeft = cPos.x + ((child.getDefaultSize() * scale) / 2);
				yLeft = cPos.y;
				yMin = Math.min(yMin, yLeft);
			}

			if (cPos.x > xRight) {
				xRight = cPos.x + ((child.getDefaultSize() * scale) / 2);
				yRight = cPos.y;
				yMin = Math.min(yMin, yRight);
			}
		}

		Point nPos = new Point(node.getAbsolutePosition().x, node.getAbsolutePosition().y);
		applyTransformations(nPos);

		texturePath.moveTo(nPos.x + ((node.getPNode().getDefaultSize() * scale) / 2), nPos.y + ((node.getPNode().getDefaultSize() * scale) / 2));
		texturePath.lineTo(xLeft + (Link2D.TRAPEZOID_DELTA * scale), nPos.y + ((node.getPNode().getDefaultSize() * scale) / 2));
		texturePath.lineTo(xLeft, yMin);
		texturePath.lineTo(xRight, yMin);
		texturePath.lineTo(xRight - (Link2D.TRAPEZOID_DELTA * scale), nPos.y + ((node.getPNode().getDefaultSize() * scale) / 2));
		texturePath.lineTo(nPos.x + ((node.getPNode().getDefaultSize() * scale) / 2), nPos.y + ((node.getPNode().getDefaultSize() * scale) / 2));

		return texturePath;
	}

	/**
	 * Select and highlight a node if not selected, or else turn it back to the default display state
	 * 
	 * @param node2D
	 */
	public void togglePrimarySelection(Node2D node2D) {
		if (node2D != null) {
			pyramide.togglePrimarySelection(node2D.getPNode());
		}
		else {
			pyramide.togglePrimarySelection(null);
		}
	}

	/**
	 * Set or unset the focused node
	 * 
	 * @param node
	 */
	public void toggleFocused(Node2D node2D) {
		if (node2D != null) {
			pyramide.toggleFocused(node2D.getPNode());
		}
		else {
			pyramide.toggleFocused(null);
		}
	}

	/**
	 * Select and highlight a node if not selected, or else turn it back to the default display state
	 * 
	 * @param node2D
	 */
	public void toggleSecondarySelection(Node2D node2D) {
		if (node2D != null) {
			pyramide.toggleSecondarySelection(node2D.getPNode());
		}
		else {
			pyramide.toggleSecondarySelection(null);
		}
	}

	/**
	 * Sets the {@link Node2D} (and all its children) color to selection color
	 * 
	 * @param node
	 */
	protected void switchSelected(Node2D node2D) {
		pyramide.switchSelected(node2D.getPNode());
	}

	/**
	 * Sets the {@link Node2D} (and all its children) color to selection color
	 * 
	 * @param node
	 */
	protected void switchSecondarySelected(Node2D node2D) {
		pyramide.switchSecondarySelected(node2D.getPNode());
	}

	/**
	 * Sets a primary selected {@link Node2D} (and all its children) color to drawing color
	 * 
	 * @param node
	 */
	protected void primaryUnselect(Node2D node2D) {
		pyramide.primaryUnselect(node2D.getPNode());
	}

	/**
	 * Sets a secondary selected {@link Node2D} (and all its children) color to drawing color
	 * 
	 * @param node
	 */
	protected void secondaryUnselect(Node2D node2D) {
		pyramide.secondaryUnselect(node2D.getPNode());
	}

	/**
	 * Verifies that a node is primary selected with color comparison
	 * 
	 * @param node
	 * @return true if the node color (alpha not taken into account) corresponds to <i>selectionColor</i>, or else false
	 */
	public boolean havePrimarySelectionPaint(Node2D node) {
		return pyramide.havePrimarySelectionPaint(node.getPNode());
	}

	/**
	 * Verifies that a node is secondary selected with color comparison
	 * 
	 * @param node
	 * @return true if the node color (alpha not taken into account) corresponds to <i>secondarySelectionColor</i>, or else false
	 */
	public boolean haveSecondarySelectionPaint(Node2D node) {
		return pyramide.haveSecondarySelectionPaint(node.getPNode());
	}

	/**
	 * Calculates the node x coordinate
	 * 
	 * @param node
	 * @return the middle value of the parents x coordinates
	 */
	protected float calculateX(PyramideNode node) throws PyramideViewer2DException {

		float xMin = Float.POSITIVE_INFINITY;
		float xMax = Float.NEGATIVE_INFINITY;

		for (PyramideNode child : node.getChildren()) {
			Point childCoord = assocNodes.get(child).getAbsolutePosition();

			if (childCoord == null) {
				throw new PyramideViewer2DException("Couldn't calculate x coordinate for " + node.getName() + "(id=" + node.getID() + ")" + "because the child node" + child.getName() + "(id=" + child.getID() + ") has not been handled yet");
			}

			if (xMin > childCoord.x) {
				xMin = (float) childCoord.x;
			}

			if (xMax < childCoord.x) {
				xMax = (float) childCoord.x;
			}
		}

		return (xMin + xMax) / 2;
	}

	/**
	 * Focuses the view on a node family
	 * 
	 * @param head
	 *            the head of the family to focus on
	 */
	public void focusOnFamily(Node2D head) {

		Rectangle bBox = getFamilyBoundingBox(head);

		int dx = (getWidth() / 2) - (bBox.x + (bBox.width / 2));
		int dy = (getHeight() / 2) - (bBox.y + (bBox.height / 2));

		animationMonitor.queueAnimation(animationBuilder.getTranslationAnimation(new Point(translation.x + dx, translation.y + dy)));
	}

	/**
	 * Constructs a node family bounding box
	 * 
	 * @param node
	 *            the family head node
	 * @return the family bounding box
	 */
	protected Rectangle getFamilyBoundingBox(Node2D node) {

		Point limits = familyLimitCoordinates(node);

		Rectangle bBox = new Rectangle(limits.x, node.getLocation().y, limits.y - limits.x, base.getFirst().getLocation().y - node.getLocation().y);

		return bBox;
	}

	/**
	 * Finds the highest and lowest x coordinate in a node family
	 * 
	 * @param node
	 *            the head of the family
	 * @return a point where x and y are respectively the lowest and the highest x coordinates
	 */
	protected Point familyLimitCoordinates(Node2D node) {

		// Point containing xmin & xmax
		Point limits = new Point(node.getLocation().x, (node.getLocation().x + node.getWidth()));

		if (node.getPNode().getChildren() != null) {
			for (PyramideNode child : node.getPNode().getChildren()) {
				Point childLimits = familyLimitCoordinates(assocNodes.get(child));
				limits.setLocation(Math.min(limits.x, childLimits.x), Math.max(limits.y, childLimits.y));
			}
		}

		return limits;
	}

	/**
	 * Avoid that a component is drawn outside the visible zone
	 * 
	 * @param center
	 *            the center of the component to be tested
	 * @param width
	 *            the component width
	 * @param height
	 *            the component height
	 */
	protected void avoidGraphicOverflow(Point center, int width, int height) {
		int dx = 0;
		int dy = 0;

		boolean top = false, bottom = false, left = false, right = false;

		// Top
		if ((center.y - (height / 2)) < MARGINS) {
			top = true;
			dy += MARGINS - (center.y - (height / 2));
		}

		// Bottom
		if ((center.y + (height / 2)) > (getHeight() - MARGINS)) {
			bottom = true;
			dy += (getHeight() - MARGINS) - (center.y + (height / 2));
		}

		// Left
		if ((center.x - (width / 2)) < MARGINS) {
			left = true;
			dx += MARGINS - (center.x - (width / 2));
		}

		// Right
		if ((center.x + (width / 2)) > (getWidth() - MARGINS)) {
			right = true;
			dx += (getWidth() - MARGINS) - (center.x + (width / 2));
		}

		if ((top || bottom || left || right) && (dx != 0 || dy != 0)) {
			Point to = new Point(translation.x + dx, translation.y + dy);
			animationMonitor.queueAnimation(animationBuilder.getTranslationAnimation(to));
		}
	}

	/**
	 * Hides all nodes info
	 */
	public void hideAllNodesInfo() {

		for (Iterator<Entry<PyramideNode, Node2D>> iter = assocNodes.entrySet().iterator(); iter.hasNext();) {
			Entry<PyramideNode, Node2D> entry = iter.next();
			Node2D node2D = entry.getValue();

			if (node2D.getStarDrawer().isShowing()) {
				animationMonitor.queueAnimation(node2D.getAnimator().getCloseStarAnimation());
			}
		}
	}

	/**
	 * Adds an {@link Annotation} to the view
	 * 
	 * @param annotation
	 *            the {@link Annotation} to be added
	 */
	public void addAnnotation(Annotation annotation) {

		annotation.setVisible(true);
		annotation.setBounds(getWidth() / 2, getHeight() / 2, annotation.getDefaultWidth(), annotation.getDefaultHeight());
		add(annotation, 0);

		Point position = annotation.getLocation();
		invertTransformations(position);
		annotation.setAbsolutePosition(position);

		for (ControlPoint cp : annotation.getControlPoints()) {
			add(cp, 0);
			cp.setBounds((int) (annotation.getX() + (annotation.getWidth() * cp.getRelativeXPosition())), (int) (annotation.getY() + (annotation.getHeight() * cp.getRelativeYPosition())), ControlPoint.DEFAULT_SIZE, ControlPoint.DEFAULT_SIZE);
		}

		annotation.correctControlPointPositions();

		annotation.setPreferredScale(scale);
		annotation.setPreferredTranslation(new Point(translation.x, translation.y));

		annotations.add(annotation);

		annotation.repaint();
	}

	/**
	 * Updates an {@link Annotation} (sets the bounds, the control points positions, ...)
	 * 
	 * @param annotation
	 *            the {@link Annotation} to be updated
	 */
	public void updateAnnotation(Annotation annotation) {
		for (ControlPoint cp : annotation.getControlPoints()) {
			cp.setBounds((int) (annotation.getX() + (annotation.getWidth() * cp.getRelativeXPosition())), (int) (annotation.getY() + (annotation.getHeight() * cp.getRelativeYPosition())), ControlPoint.DEFAULT_SIZE, ControlPoint.DEFAULT_SIZE);
		}

		Point position = annotation.getLocation();
		invertTransformations(position);
		annotation.setAbsolutePosition(position);

		annotation.correctControlPointPositions();

		annotation.repaint();
	}

	/**
	 * Removes an {@link Annotation} from the view
	 * 
	 * @param annotation
	 *            the {@link Annotation} to be removed
	 */
	public void removeAnnotation(Annotation annotation) {
		for (ControlPoint cp : annotation.getControlPoints()) {
			remove(cp);
		}

		annotations.remove(annotation);
		remove(annotation);

		repaint();

		updateAnnotationPreviewDialog();
	}

	/**
	 * Saves an annotation preview (i.e. a screenshot of the current view)
	 * 
	 * @param annotation
	 *            the annotation to save the preview of
	 */
	public void saveAnnotationPreview(Annotation annotation) {

		boolean saveOk = false;

		try {
			Robot screenshooter = new Robot();

			BufferedImage shot = screenshooter.createScreenCapture(new Rectangle(getLocationOnScreen().x, getLocationOnScreen().y, getWidth(), getHeight()));

			File target = new File(PyramideViewer2D.APPLICATION_SAVE_PATH + File.separator + PyramideViewer2D.PREVIEW_DIRECTORY + File.separator + PyramideViewer2D.PREVIEW_PREFIX + new String(annotation.getID()).replace(':', '_') + ".jpg");
			target.createNewFile();

			saveOk = ImageIO.write(shot, "jpg", new FileOutputStream(target));
		}
		catch (AWTException e) {
			e.printStackTrace();
		}
		catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		catch (IOException e) {
			e.printStackTrace();
		}

		if (saveOk) {
			updateAnnotationPreviewDialog();
		}
	}

	/**
	 * Focuses the view on a given {@link Annotation}
	 * 
	 * @param annotation
	 *            the {@link Annotation} to focus on
	 */
	public void goToAnnotation(Annotation annotation) {
		animationMonitor.queueAnimation(animationBuilder.getTranslationAnimation(annotation.getPreferredTranslation()));
		animationMonitor.queueAnimation(animationBuilder.getScaleAnimation(annotation.getPreferredScale()));
	}

	/**
	 * Launches a {@link Thread} which updates the viewer {@link AnnotationsPreviewDialog}
	 */
	public void updateAnnotationPreviewDialog() {
		new Thread() {
			@Override
			public void run() {
				viewer.getAnnotationsPreviewDialog().updateContent();
			}
		}.start();
	}

	/**
	 * Sets a {@link Node2D} user label
	 * 
	 * @param nodeID
	 *            a node identifier
	 * @param label
	 *            the label to be set
	 */
	public void setUserLabel(int nodeID, String label) {
		assocNodes.get(pyramide.getNode(nodeID)).getUserLabel().setText(label);
	}

	/**
	 * Applies the recorded transformations (currently translation & scale) on a point
	 */
	public void applyTransformations(Point p) {
		applyTranslation(p);
		applyScale(p);
	}

	/**
	 * Inverts the recorded transformations (currently translation & scale) on a point
	 */
	public void invertTransformations(Point p) {
		invertScale(p);
		invertTranslation(p);
	}

	/**
	 * Transforms a point coordinates accordingly to the view <i>translation</i> parameter
	 * 
	 * @param p
	 *            the point to be transformed
	 */
	protected void applyTranslation(Point p) {
		int nx = p.x + translation.x;
		int ny = p.y + translation.y;

		p.setLocation(nx, ny);
	}

	/**
	 * Transforms a point coordinates accordingly to the view <i>translation</i> parameter invert
	 * 
	 * @param p
	 *            the point to be transformed
	 */
	protected void invertTranslation(Point p) {
		int nx = p.x - translation.x;
		int ny = p.y - translation.y;

		p.setLocation(nx, ny);
	}

	/**
	 * Transforms a point coordinates accordingly to the view <i>scale</i> parameter
	 * 
	 * @param p
	 *            the point to be transformed
	 */
	protected void applyScale(Point p) {

		/* 1 - Scaling */
		float nx = p.x * scale;
		float ny = p.y * scale;

		/* 2 - Recentering */
		nx += (getWidth() / 2) * (1 - scale);
		ny += (getHeight() / 2) * (1 - scale);

		p.setLocation(nx, ny);
	}

	/**
	 * Transforms a point coordinates accordingly to the view <i>scale</i> parameter invert
	 * 
	 * @param p
	 *            the point to be transformed
	 */
	protected void invertScale(Point p) {

		/* 1 - Recentering */
		float nx = p.x - ((getWidth() / 2) * (1 - scale));
		float ny = p.y - ((getHeight() / 2) * (1 - scale));

		/* 2 - Scaling */
		nx /= scale;
		ny /= scale;

		p.setLocation(nx, ny);
	}

	/**
	 * Zoom in (use USER_SCALE_STEP constant)
	 */
	public void zoomIn() {
		scale *= USER_SCALE_STEP;

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Zoom out (use USER_SCALE_STEP constant)
	 */
	public void zoomOut() {

		scale *= 1 / USER_SCALE_STEP;

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Translates the view up corresponding to USER_TRANSLATION_STEP
	 */
	public void goUp() {
		translation = new Point(translation.x, translation.y - USER_TRANSLATION_STEP);

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Translates the view down corresponding to USER_TRANSLATION_STEP
	 */
	public void goDown() {
		translation = new Point(translation.x, translation.y + USER_TRANSLATION_STEP);

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Translates the view left corresponding to USER_TRANSLATION_STEP
	 */
	public void goLeft() {
		translation = new Point(translation.x - USER_TRANSLATION_STEP, translation.y);

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Translates the view right corresponding to USER_TRANSLATION_STEP
	 */
	public void goRight() {
		translation = new Point(translation.x + USER_TRANSLATION_STEP, translation.y);

		positionSecondaryObjects();

		repaint();
	}

	/**
	 * Getter for the view {@link PyramideViewer2D}
	 * 
	 * @return the {@link PyramideViewer2D} containing the view
	 */
	public PyramideViewer2D getViewer() {
		return viewer;
	}

	/**
	 * Getter for the view associated {@link Pyramide}
	 * 
	 * @return the {@link Pyramide}
	 */
	public Pyramide getPyramide() {
		return pyramide;
	}

	/**
	 * Getter for the current primary selected {@link Node2D}
	 * 
	 * @return the current primary selected {@link Node2D}
	 */
	public Node2D getSelection() {
		return assocNodes.get(pyramide.getSelection());
	}

	/**
	 * Getter for the current focused {@link Node2D}
	 * 
	 * @return the current focused {@link Node2D}
	 */
	public Node2D getFocused() {
		return assocNodes.get(pyramide.getFocused());
	}

	/**
	 * Setter for the focused node
	 * 
	 * @param node
	 *            the current focused {@link Node2D}
	 */
	public void setFocused(Node2D node) {
		if (node != null) {
			pyramide.setFocused(node.getPNode());
		}
		else {
			pyramide.setFocused(null);
		}
	}

	/**
	 * Getter for the secondary {@link PyramideNode} selection list
	 * 
	 * @return the secondary {@link PyramideNode} selection list
	 */
	public LinkedList<PyramideNode> getSecondarySelectionList() {
		return pyramide.getSecondarySelectionList();
	}

	/**
	 * Getter for the view {@link Node2D} list
	 * 
	 * @return the {@link Node2D} list
	 */
	public HashMap<Integer, Node2D> getNodes() {
		return nodes;
	}

	/**
	 * Getter for the view {@link HashMap} associating {@link PyramideNode} to {@link Node2D}
	 * 
	 * @return the {@link HashMap}
	 */
	public HashMap<PyramideNode, Node2D> getNodeHashMap() {
		return this.assocNodes;
	}

	/**
	 * Getter for the view {@link AnimationMonitor}
	 * 
	 * @return the view {@link AnimationMonitor}
	 */
	public AnimationMonitor getAnimationMonitor() {
		return animationMonitor;
	}

	/**
	 * Getter for the view {@link Pyramide2DSearchTool}
	 * 
	 * @return the view {@link Pyramide2DSearchTool}
	 */
	public Pyramide2DSearchTool getSearchTool() {
		return searchTool;
	}

	/**
	 * Getter for the view animator
	 * 
	 * @return the {@link Pyramide2DAnimator} assigned to the view
	 */
	public Pyramide2DAnimator getAnimationBuilder() {
		return animationBuilder;
	}

	/**
	 * Getter for the view associated {@link MiniView2D}
	 * 
	 * @return the associated {@link MiniView2D}
	 */
	public MiniView2D getMiniView() {
		return miniView;
	}

	/**
	 * Setter for the view {@link MiniView2D}
	 * 
	 * @param miniView
	 *            the {@link MiniView2D} to be associated with
	 */
	public void setMiniView(MiniView2D miniView) {
		this.miniView = miniView;
	}

	/**
	 * Getter for the view node barrel
	 * 
	 * @return the {@link Barrel} assigned to the view
	 */
	public Barrel getBarrel() {
		return barrel;
	}

	/**
	 * Getter for the view annotation list
	 * 
	 * @return the {@link Vector} containing all the view {@link Annotation}
	 */
	public Vector<Annotation> getAnnotations() {
		return annotations;
	}

	/**
	 * Getter for the view node {@link Shelf2D}
	 * 
	 * @return the {@link Shelf2D} assigned to the view
	 */
	public Shelf2D getShelf() {
		return shelf;
	}

	/**
	 * Getter for the {@link Link2D} list
	 * 
	 * @return the {@link Link2D} list
	 */
	public LinkedList<Link2D> getLinks() {
		return links;
	}

	/**
	 * Getter for the current link shape
	 * 
	 * @return an integer corresponding to the current links shape
	 */
	public int getLinkShape() {
		return pyramide.getLinkShape();
	}

	/**
	 * Setter for the link shape
	 * 
	 * @param linkShape
	 *            a link shape constant
	 */
	public void setLinkShape(int linkShape) {

		pyramide.setLinkShape(linkShape);

		for (Link2D link : links) {
			link.setShape(linkShape);
		}
	}

	/**
	 * Getter for the <i>pyramide</i> {@link Grid2D}
	 * 
	 * @return the associated {@link Grid2D}
	 */
	public Grid2D getGrid() {
		return this.grid;
	}

	/**
	 * Getter for the <i>useGrid</i> indicator
	 * 
	 * @return true if using grid, or else false
	 */
	public boolean gridEnabled() {
		return pyramide.gridEnabled();
	}

	/**
	 * Setter for the grid use
	 * 
	 * @param enable
	 *            true to enable grid use, or else false
	 */
	public void enableGrid(boolean enable) {
		pyramide.enableGrid(enable);
	}

	/**
	 * Getter for the texture state
	 * 
	 * @return
	 */
	public boolean texturesEnabled() {
		return pyramide.texturesEnabled();
	}

	/**
	 * Setter for the texture state
	 * 
	 * @param enable
	 *            true to enable textures, false to disable them
	 */
	public void enableTextures(boolean enable) {
		pyramide.enableTextures(enable);
	}

	/**
	 * Getter for the current drawing color
	 * 
	 * @return the current {@link Color} used to draw
	 */
	public Color getDrawingColor() {
		return pyramide.getLinkColor();
	}

	/**
	 * Getter for the focused node drawing color
	 * 
	 * @return the current {@link Color} used to draw the focused node (mouse over)
	 */
	public Color getFocusedDrawingColor() {
		return pyramide.getNodePassiveColor();
	}

	/**
	 * Setter for the current selection
	 * 
	 * @param selection
	 *            the current selected {@link Node2D}
	 */
	public void setSelection(Node2D selection) {
		if (selection != null) {
			pyramide.setSelection(selection.getPNode());
		}
		else {
			pyramide.setSelection(null);
		}
	}

	/**
	 * Getter for the current selection color
	 * 
	 * @return the current {@link Color} used to draw the primary selected node
	 */
	public Color getPrimarySelectionColor() {
		return pyramide.getNodePrimaryActiveColor();
	}

	/**
	 * Getter for the current secondary selection color
	 * 
	 * @return the current {@link Color} used to draw
	 */
	public Color getSecondarySelectionColor() {
		return pyramide.getNodeSecondaryActiveColor();
	}

	/**
	 * Getter for the focused node selection color
	 * 
	 * @return the current {@link Color} used to draw
	 */
	public Color getFocusedSelectionColor() {
		return pyramide.getNodePrimaryActiveColor();
	}

	/**
	 * Getter for the focused node secondary selection color
	 * 
	 * @return the current {@link Color} used to draw a secondary selected node when focused (mouse over + secondary selected)
	 */
	public Color getFocusedSecondarySelectionColor() {
		return pyramide.getNodeSecondaryActiveColor();
	}

	/**
	 * Getter for the default drawing color
	 * 
	 * @return the default drawing {@link Color}
	 */
	public Color getDefaultDrawingColor() {
		return pyramide.getDefaultDrawingColor();
	}

	/**
	 * Setter for the default drawing color
	 * 
	 * @param defaultDrawingColor
	 *            the new {@link Color} to be applied
	 */
	public void setDefaultDrawingColor(Color defaultDrawingColor) {
		pyramide.setDefaultDrawingColor(defaultDrawingColor);
		setDrawingColor(defaultDrawingColor);
	}

	/**
	 * Setter for the drawing color. <br>
	 * </br> Also recolors all the nodes <br>
	 * </br> <i>NB : the focused node alpha composite stays fixed at 1.0f</i>
	 * 
	 * @param drawingColor
	 *            the drawing {@link Color} to be set
	 */
	public void setDrawingColor(Color drawingColor) {
		pyramide.setLinkColor(drawingColor);
		recolorNodes();
	}

	/**
	 * Recolors all nodes
	 */
	public void recolorNodes() {
		for (Iterator<Entry<PyramideNode, Node2D>> iter = assocNodes.entrySet().iterator(); iter.hasNext();) {
			Entry<PyramideNode, Node2D> entry = iter.next();
			Node2D node2D = entry.getValue();

			if (!havePrimarySelectionPaint(node2D) && !haveSecondarySelectionPaint(node2D)) { // Unselected node
				if (getFocused() == node2D) {
					node2D.setColor(getFocusedDrawingColor());
				}
				else {
					node2D.setColor(getDrawingColor());
				}
			}
			else {// Selected node
				if (havePrimarySelectionPaint(node2D)) { // Primary selection
					if (getFocused() == node2D) {
						node2D.setColor(getFocusedSelectionColor());
					}
					else {
						node2D.setColor(getPrimarySelectionColor());
					}
				}
				else { // Secondary selection
					if (getFocused() == node2D) {
						node2D.setColor(getFocusedSecondarySelectionColor());
					}
					else {
						node2D.setColor(getSecondarySelectionColor());
					}
				}
			}
		}
	}

	/**
	 * Setter for the primary selection color. <br>
	 * </br>Sets also the color of the nodes. <br>
	 * </br> <i>NB : the focused node alpha composite stays fixed at 1.0f</i>
	 * 
	 * @param primarySelectionColor
	 *            a {@link Color}
	 */
	public void setPrimarySelectionColor(Color primarySelectionColor) {
		pyramide.setNodePrimaryActiveColor(primarySelectionColor);

		if (getSelection() != null) {
			switchSelected(getSelection());
		}
	}

	/**
	 * Setter for the secondary selection color. <br>
	 * </br>Sets also the color of the nodes. <br>
	 * </br> <i>NB : the focused node alpha composite stays fixed at 1.0f</i>
	 * 
	 * @param secondarySelectionColor
	 *            a {@link Color}
	 */
	public void setSecondarySelectionColor(Color secondarySelectionColor) {
		pyramide.setNodeSecondaryActiveColor(secondarySelectionColor);

		for (PyramideNode secondarySelection : getSecondarySelectionList()) {
			switchSecondarySelected(assocNodes.get(secondarySelection));
		}
	}

	/**
	 * Getter for the scale parameter
	 * 
	 * @return the current scale factor
	 */
	public float getScale() {
		return scale;
	}

	/**
	 * Setter for the scale parameter
	 * 
	 * @param scale
	 *            the scale to be applied
	 */
	public void setScale(float scale) {
		this.scale = scale;
	}

	/**
	 * Sets the scaling state of the view
	 * 
	 * @param scaling
	 */
	public void setScaling(boolean scaling) {
		this.scaling = scaling;
	}

	/**
	 * Getter for the scaling state of the view
	 * 
	 * @return true if the view is current being scaled, or else false
	 */
	public boolean isScaling() {
		return scaling;
	}

	/**
	 * Getter for the translation parameter
	 * 
	 * @return the current translation Point
	 */
	public Point getTranslation() {
		return translation;
	}

	/**
	 * Setter for the translation parameter
	 * 
	 * @param translation
	 *            translation {@link Point} (i.e. translation vector)
	 */
	public void setTranslation(Point translation) {
		this.translation = translation;
	}

	/**
	 * Sets the translating state of the view
	 * 
	 * @param translating
	 *            true or false
	 */
	public void setTranslating(boolean translating) {
		this.translating = translating;
	}

	/**
	 * Getter for the translating state of the view
	 * 
	 * @return true if the view is currently being translated, or else false
	 */
	public boolean isTranslating() {
		return translating;
	}

	/**
	 * Getter for the <i>Pyramide</i> ID
	 * 
	 * @return the <i>Pyramide</i> ID {@link String}
	 */
	public String getID() {
		return pyramide.getID();
	}

	/**
	 * Getter for the <i>Pyramide</i> levels
	 * 
	 * @return the <i>Pyramide</i> levels {@link LinkedList}
	 */
	public LinkedList<LinkedList<Node2D>> getAllLevels() {
		return levels;
	}

	/**
	 * Getter for the <i>Pyramide</i> base
	 * 
	 * @return the <i>Pyramide</i> base
	 */
	public LinkedList<Node2D> getBase() {
		return base;
	}

	/**
	 * Setter for the <i>Pyramide</i> ID
	 * 
	 * @param id
	 *            the <i>Pyramide</i> ID {@link String}
	 */
	public void setID(String id) {
		pyramide.setID(id);
	}

	/**
	 * Gets a <i>Pyramide</i> node
	 * 
	 * @param id
	 * @return the wanted node or null if it does'nt exist
	 */
	public Node2D getNode(int id) {
		return nodes.get(id);
	}

	/**
	 * Gets the pyramide height
	 * 
	 * @return the associated {@link Pyramide} height
	 */
	public double getPyramideHeight() {
		return pyramide.getHeight();
	}

	/**
	 * Gets the variables
	 * 
	 * @return
	 */
	public HashMap<Integer, PyramideVariable> getPyramideVariables() {
		return pyramide.getPyramideVariables();
	}

	/**
	 * Interactive event
	 */
	public void mouseClicked(MouseEvent e) {
		requestFocus();

		viewer.getInformationZone().remove(viewer.getInformationZone().getComponent(0));
		viewer.getInformationZone().repaint();

		viewer.getInformationZone().add(new JLabel());
		viewer.getInformationZone().repaint();

		searchTool.setSearching(false);

		if (e.getButton() == MouseEvent.BUTTON1 && !shelf.isShowing()) {

			togglePrimarySelection(getSelection());

			setFocused(null);

			hideAllNodesInfo();

			animationMonitor.queueAnimation(animationBuilder.getColorAnimation(getDefaultDrawingColor()));
		}

		repaint();
	}

	/**
	 * Interactive event
	 */
	public void mouseEntered(MouseEvent e) {
		// NOTHING FOR NOW
	}

	/**
	 * Interactive event
	 */
	public void mouseExited(MouseEvent e) {
		// NOTHING FOR NOW
	}

	/**
	 * Interactive event
	 */
	public void mousePressed(MouseEvent e) {

		if (e.getSource() instanceof Pyramide2D && e.getButton() == MouseEvent.BUTTON3) {
			dragging = true;
			setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
		}

		mouseX = e.getX();
		mouseY = e.getY();
	}

	/**
	 * Interactive event
	 */
	public void mouseReleased(MouseEvent e) {
		if (e.getSource() instanceof Pyramide2D && e.getButton() == MouseEvent.BUTTON3) {
			dragging = false;
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
		}
	}

	/**
	 * Interactive event
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {

		int direction = e.getWheelRotation();

		if (direction < 0) { // Wheel up
			zoomIn();
		}
		else { // Wheel down
			zoomOut();
		}
	}

	/**
	 * Interactive event
	 */
	public void mouseDragged(MouseEvent e) {
		if (e.getSource() instanceof Pyramide2D && dragging) {

			translation = new Point(translation.x + (int) ((e.getX() - mouseX) / scale), translation.y + (int) ((e.getY() - mouseY) / scale));

			mouseX = e.getX();
			mouseY = e.getY();

			positionSecondaryObjects();

			repaint();
		}
	}

	/**
	 * Interactive event
	 */
	public void mouseMoved(MouseEvent e) {
		if (barrel.isOpened()) {
			if (!barrel.getActivationZone().contains(e.getPoint())) {
				barrel.stopAnimationTimer();
				animationMonitor.queueAnimation(barrel.getCloseBarrelAnimation());
			}
		}
	}

	/**
	 * Interactive event
	 */
	public void keyPressed(KeyEvent e) {

		switch (e.getKeyCode()) {

		case KeyEvent.VK_E:

			if (!shelf.isShowing()) {
				animationMonitor.queueAnimation(animationBuilder.getColorAnimation(new Color(((float) getDefaultDrawingColor().getRed()) / 255.0f, ((float) getDefaultDrawingColor().getGreen()) / 255.0f, ((float) getDefaultDrawingColor().getBlue()) / 255.0f, FADING)));
				animationMonitor.queueAnimation(shelf.getOpeningAnimation());
			}
			else {
				animationMonitor.queueAnimation(shelf.getClosingAnimation());
				animationMonitor.queueAnimation(animationBuilder.getColorAnimation(getDefaultDrawingColor()));
			}

			break;

		case KeyEvent.VK_T:

			enableTextures(!texturesEnabled());

			if (texturesEnabled()) { // Texture switch on
				traceAllTexturePaths();
			}

			setDrawingColor(getDefaultDrawingColor());

			repaint();

			PyramideViewer2DPreferencesPanel.getInstance().updatePreferences();

			break;

		case KeyEvent.VK_G:

			if ((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) == 0) {
				enableGrid(!gridEnabled());
			}
			else {
				if (grid.getScaleType() == Grid2D.LINEAR_SCALE) {
					grid.setScaleType(Grid2D.LOGARITHMIC_SCALE);
				}
				else {
					grid.setScaleType(Grid2D.LINEAR_SCALE);
				}

				miniView.positionNodes();
				positionNodes();
			}

			repaint();

			PyramideViewer2DPreferencesPanel.getInstance().updatePreferences();

			break;

		case KeyEvent.VK_F5:

			// Reinitialization
			togglePrimarySelection(getSelection());

			setFocused(null);

			hideAllNodesInfo();

			animationMonitor.flushAnimations();

			animationMonitor.queueAnimation(animationBuilder.getScaleAnimation(1.0f));
			animationMonitor.queueAnimation(animationBuilder.getTranslationAnimation(new Point(0, 0)));
			animationMonitor.queueAnimation(animationBuilder.getColorAnimation(getDefaultDrawingColor()));

			break;

		case KeyEvent.VK_L:

			if (getLinkShape() == Link2D.TRIANGULAR) {
				setLinkShape(Link2D.TRAPEZOIDAL);
			}
			else {
				setLinkShape(Link2D.TRIANGULAR);
			}

			for (Link2D link : links) {
				link.setShape(getLinkShape());
			}

			if (texturesEnabled()) {
				traceAllTexturePaths();
			}

			repaint();

			PyramideViewer2DPreferencesPanel.getInstance().updatePreferences();

			break;

		case KeyEvent.VK_UP:
			goUp();
			break;

		case KeyEvent.VK_DOWN:
			goDown();
			break;

		case KeyEvent.VK_LEFT:
			goLeft();
			break;

		case KeyEvent.VK_RIGHT:
			goRight();
			break;

		case KeyEvent.VK_ADD:
			zoomIn();
			break;

		case KeyEvent.VK_SUBTRACT:
			zoomOut();
			break;
		}
	}

	/**
	 * Interactive event (unused)
	 */
	public void keyReleased(KeyEvent e) {
		// NOTHING FOR NOW

	}

	/**
	 * Interactive event (unused)
	 */
	public void keyTyped(KeyEvent e) {
		// NOTHING FOR NOW
	}

	/**
	 * Releases all the <i>Pyramide</i> resources
	 */
	public void dispose() {

		animate = false;
		animationMonitor.flushAnimations();

		/* Disposal of the nodes */
		for (Iterator<Entry<Integer, Node2D>> entries = nodes.entrySet().iterator(); entries.hasNext();) {
			Node2D node = entries.next().getValue();
			node.dispose();
		}

		/* Clearing the lists */
		base.clear();

		for (LinkedList<Node2D> level : levels) {
			level.clear();
		}

		levels.clear();

		for (Link2D link : links) {
			link.dispose();
		}

		/*
		 * Shelf
		 */
		shelf.dispose();

		/*
		 * Barrel
		 */
		barrel.dispose();

		/*
		 * Textures
		 */
		distanceTexturePaths.clear();
		brickTexture.flush();
		marbleTexture.flush();

		removeAll();
	}

	@Override
	public String toString() {
		return pyramide.toString();
	}
}
